/* 
 * Copyright 2011 kOSMik.
 * This file is part of kOSMik.
 * 
 * kOSMik is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * kOSMik is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with kOSMik.  If not, see <http://www.gnu.org/licenses/>.
 *  
 * kOSMik:
 * Open Street Map - Editor.
 * http://code.google.com/p/swp-dv-ws2010-osm-2/
 */

package de.fuberlin.kosmik.data.osmt;

import android.content.ContentValues;
import android.database.Cursor;

/**
 * This class encapsulate an row of the Key table and defines read and write
 * operations.
 * 
 * @author Johannes
 * 
 */
public class OsmtKey implements OsmtTable {

	/** Name of table Key. */
	static final String TABLE = "Key";
	/** 1. Column name. */
	static final String COLUMN_OSM_NAME = "Key.osm_name";
	/** 2. Column name. */
	static final String COLUMN_USE = "Key.use_stat";
	/** 3. Column name. */
	static final String COLUMN_WIKI = "Key.wiki_uri";
	/** 4. Column name. */
	static final String COLUMN_TYPE = "Key.type";
	private final OsmtDatabase osmtDatabase;
	private final OsmtKeyRecord record = new OsmtKeyRecord();

	/** Constant to set the keys codomain defined by enums. */
	public static final int ENUM_CODOMAIN = 1;
	/** Constant to set the keys codomain as free text. */
	public static final int TEXT_CODOMAIN = 2;
	/** Constant to set the keys codomain as a range like integer. */
	public static final int RANGE_CODOMAIN = 4;

	/**
	 * This constructor considered all attribute without the use attribute. If
	 * an key with this name already exists in the database table, we have a
	 * conflict. Otherwise a new key will be created and inserted into the
	 * database.
	 * 
	 * A conflict will handled as described below. If the type matches each
	 * other, the the wiki is updated in case of inequality. If the types does
	 * not match, an OsmtConflictException will be thrown.
	 * 
	 * @param osmtData specifies the database
	 * @param osmName The name of the key.
	 * @param type The type specify the codomain from which the values can be
	 * @param wiki An url to a wiki page.
	 * @throws OsmtObjectException if the object creation fail
	 * 
	 */
	public OsmtKey(OsmtData osmtData, String osmName, int type, String wiki)
			throws OsmtObjectException {
		super();
		this.osmtDatabase = ((OsmtDataImpl) osmtData).getDatabase();
		if (!set(osmName, type, wiki))
			throw new OsmtObjectException(String.format("can neither load nor create key %s.",
					osmName));
	}

	/**
	 * Constructor.
	 * 
	 * @param osmtData specifies the database
	 * @param osmKeyRecord The record containing the key of an table row. This
	 *            constructor considered all attribute without the use
	 *            attribute. If an key with this name already exists in the
	 *            database table, we have a conflict. Otherwise a new key will
	 *            be created and inserted into the database.
	 * 
	 *            A conflict will handled as described below. If the type
	 *            matches each other, the the wiki is updated in case of
	 *            inequality. If the types does not match, an
	 *            OsmtConflictException will be thrown.
	 * @throws OsmtObjectException if the object creation fail
	 * 
	 */
	public OsmtKey(OsmtData osmtData, OsmtKeyRecord osmKeyRecord) throws OsmtObjectException {
		this(osmtData, osmKeyRecord.osmName, osmKeyRecord.type, osmKeyRecord.wiki);
	}

	/**
	 * Constructor. Loads an key by name in case of existance.
	 * 
	 * 
	 * @param osmtDatabase specifies the database
	 * @param osmName The name of the key
	 * @throws OsmtObjectException if the object creation fail
	 */
	public OsmtKey(OsmtDatabase osmtDatabase, String osmName) throws OsmtObjectException {
		super();
		this.osmtDatabase = osmtDatabase;
		if (!loadKeyByName(this, osmName))
			throw new OsmtObjectException("can not load such a key.");
	}

	/**
	 * Sets the key direct without consistency checks.
	 * 
	 * @param osmtKeyRecord contains the row to set
	 */
	void setTrusted(OsmtKeyRecord osmtKeyRecord) {
		record.osmName = osmtKeyRecord.osmName;
		record.use = osmtKeyRecord.use;
		record.type = osmtKeyRecord.type;
		record.wiki = osmtKeyRecord.wiki;
	}

	/**
	 * Sets the key direct without consistency checks.
	 * 
	 * @param osmName the osm name for the key
	 * @param type defines from which type the values for this key could be
	 * @param wiki a link to the corresponding wiki page
	 * @param use the number how often this key was used at all
	 */
	void setTrusted(String osmName, int use, int type, String wiki) {
		record.osmName = osmName;
		record.use = use;
		record.type = type;
		record.wiki = wiki;
	}

	/**
	 * Set to a new key.
	 * 
	 * @param osmName The name of the key the object should be set to. If the
	 *            key already exists in the database table, the corresponding
	 *            row will be load. Otherwise a new key will be created and
	 *            inserted into the database.
	 * @param type defines from which type the values for this key could be
	 * @param wiki a link to the corresponding wiki page
	 * @return true, if the key could be set.
	 */
	public boolean set(String osmName, int type, String wiki) {
		if (loadKeyByName(this, osmName)) {
			if ((type == 0) || this.record.type == type) {
				// equal types is ok
				return true;
			}
			// throw new OsmtConflictException(String.format(
			// "A key with type %d already exists. The asked type %d does not fit.",
			// Integer.valueOf(record.type), Integer.valueOf(type)));
		}
		record.osmName = osmName;
		record.use = 0;
		record.type = type;
		record.wiki = wiki;
		return tryInsertAsNewKey(this);
	}

	/**
	 * Set to a new key. Ignores the use attribute inside the OsmtKeyRecord
	 * 
	 * @param osmtKeyRecord the structure that contain the data. Furthermore, it
	 *            is like {@link OsmtKey#set(String, int, String)}
	 * @return true, if the value could be set.
	 */
	public boolean set(OsmtKeyRecord osmtKeyRecord) {
		return set(osmtKeyRecord.osmName, osmtKeyRecord.type, osmtKeyRecord.wiki);
	}

	/**
	 * @return the record containing the pure data
	 */
	public OsmtKeyRecord get() {
		return record;
	}

	/**
	 * Load data from db into the attributes of the given object.
	 * 
	 * @return true, if the specified value was known by db, otherwise false
	 */
	private static boolean loadKeyByName(final OsmtKey osmtKey, final String osmName) {
		final String lookupByName = String.format("%s='%s'", COLUMN_OSM_NAME, osmName);
		final OsmtQuery query = new OsmtQuery(TABLE, null, lookupByName);
		final boolean keyFound = osmtKey.osmtDatabase.query(query,
				new OnObtainCursorListener() {

					@Override
					public boolean onObtainCursor(Cursor cursor) {
						if (cursor.moveToFirst()) {
							final int osmNameIndex = cursor.getColumnIndex(COLUMN_OSM_NAME);
							final int useIndex = cursor.getColumnIndex(COLUMN_USE);
							final int typeIndex = cursor.getColumnIndex(COLUMN_TYPE);
							final int wikiIndex = cursor.getColumnIndex(COLUMN_WIKI);
							osmtKey.setTrusted(cursor.getString(osmNameIndex),
									cursor.getInt(useIndex),
									OsmtKey.enumToType(cursor.getString(typeIndex)),
									cursor.getString(wikiIndex));
							return true;
						}
						return false;
					}
				});
		return keyFound;
	}

	/**
	 * Try to insert a key into the database specified in the given
	 * OsmKey-object.
	 * 
	 * @param osmtKey the key that should be written
	 * @return true, if a new row inserted into the database, otherwise false
	 */
	private static boolean tryInsertAsNewKey(final OsmtKey osmtKey) {
		final ContentValues contentValues = new ContentValues();
		contentValues.put(COLUMN_OSM_NAME, osmtKey.record.osmName);
		contentValues.put(COLUMN_TYPE, OsmtKey.typeToEnum(osmtKey.record.type));
		contentValues.put(COLUMN_WIKI, osmtKey.record.wiki);
		return osmtKey.osmtDatabase.insertRecord(osmtKey, contentValues);
	}

	/**
	 * Use this method to signalize the statistic, that this osm element was
	 * used by a user one more time.
	 * 
	 * @param osmtKey the key the use counter should be increment.
	 */
	public static void useOneMoreTime(final OsmtKey osmtKey) {
		final ContentValues contentValues = new ContentValues();
		final String lookupByName = String.format("%s='%s'", COLUMN_OSM_NAME,
				osmtKey.record.osmName);
		contentValues.put(COLUMN_USE, Integer.valueOf(++osmtKey.record.use));
		osmtKey.osmtDatabase.update(osmtKey, contentValues, lookupByName, null);
	}

	/**
	 * This method try to delete the key under attention of constraints.
	 * 
	 * @param osmtKey the key that should be deleted
	 * @return true, if the key removed successfully, otherwise false
	 */
	public static boolean remove(final OsmtKey osmtKey) {
		// TODO: implement later
		return true;
	}

	@Override
	public String getTableName() {
		return TABLE;
	}

	/**
	 * Convert the value of the codomain from his representation as integer into
	 * a String.
	 * 
	 * @param type of the codomain.Use one of the constants
	 *            {@link OsmtKey#ENUM_CODOMAIN}, {@link OsmtKey#TEXT_CODOMAIN}
	 *            or {@link OsmtKey#RANGE_CODOMAIN}.
	 * @return the representation as a string.
	 * 
	 */
	public static String typeToEnum(int type) {
		switch (type) {
		case ENUM_CODOMAIN:
			return "enum";
		case TEXT_CODOMAIN:
			return "text";
		case RANGE_CODOMAIN:
		default:
			return "number";
		}
	}

	/**
	 * Convert the value of the codomain from his representation as string ( in
	 * database an enum) into a integer type.
	 * 
	 * @param enumValue of the codomain. Valid Arguments are "enum", "text" and
	 *            "range".
	 * @return the representation as an integer. That could be one of the
	 *         constants {@link OsmtKey#ENUM_CODOMAIN},
	 *         {@link OsmtKey#TEXT_CODOMAIN} or {@link OsmtKey#RANGE_CODOMAIN}.
	 * 
	 */
	public static int enumToType(String enumValue) {
		if ("enum".equalsIgnoreCase(enumValue))
			return ENUM_CODOMAIN;
		if ("text".equalsIgnoreCase(enumValue))
			return TEXT_CODOMAIN;
		// if ( "number".equalsIgnoreCase( enumValue))
		return RANGE_CODOMAIN;
	}
}
